﻿using System;
using System.Collections.Generic;
using System.Text;

namespace SqliteDriver
{
	//////////////////////////////////////////////////////////
	// basic helpers ie methods that might be in a base class
	internal class TableGeneratorHelpers
	{
		/*
		1. Make all lower
		2. Capitalise 1st
		3. Capitalise all after _ space - 
		4. Remove _ space -
		*/
		internal static  string MakePascalCaseName(string databaseName)
		{
			return MakePascalCaseName(databaseName, false); 
		}
		
		internal static  string MakePascalCaseName(string databaseName, bool depluralise)
		{
			// Don't like UPPER CASE as class name.
			if (databaseName == databaseName.ToUpper())
				databaseName = databaseName.ToLower();
			string[] splittedPhrase = databaseName.Split(' ', '-', '_');
			StringBuilder sb = new StringBuilder();
			foreach (String s in splittedPhrase)
			{
				char[] splittedPhraseChars = s.ToCharArray();
				if (splittedPhraseChars.Length > 0)
				{
					splittedPhraseChars[0] = ((new String(splittedPhraseChars[0], 1)).ToUpper().ToCharArray())[0];
				}
				sb.Append(new String(splittedPhraseChars));
			}
			// remove s if table is pluralised 
			if (depluralise)
			{
				int lastIndex = sb.Length-1;
				if (sb[lastIndex] == 's')
					sb.Remove(lastIndex, 1);
			}
			return CleanName(sb.ToString());
		}

		internal static Type ClrTypeFromSqlType(String sqlType)
		{
			// use versions that can be nullable
			// See http://www.make-awesome.com/2010/07/code-first-development-with-entity-framework-4-kicking-the-tires/
			// where the guy does a lot of type mapping testinf
			String upperSqlType = sqlType.ToUpper();
			return (upperSqlType == "INT") ? typeof(Int32) :
				   (upperSqlType == "TINYINT") ? typeof(Byte) :
				   (upperSqlType == "SMALLINT") ? typeof(Int16) :
				   (upperSqlType == "BIGINT") ? typeof(Int64) :
				   (upperSqlType == "BIT") ? typeof(Boolean) :
				   (upperSqlType == "FLOAT") ? typeof(Double) :
				   (upperSqlType == "REAL") ? typeof(float) :
				   (upperSqlType == "DECIMAL") ? typeof(Decimal) :
				   (upperSqlType == "NUEMRIC") ? typeof(Decimal) :
				   (upperSqlType == "MONEY") ? typeof(Decimal) :
				   (upperSqlType == "SMALLMONEY") ? typeof(Decimal) :
				   (upperSqlType == "DATETIME") ? typeof(DateTime) :
				   (upperSqlType == "DATE") ? typeof(DateTime) :
				   (upperSqlType == "UNIQUEIDENTIFIER") ? typeof(Guid) :
										 typeof(String);
		}
		
		internal static String MakeTypeString(DbView.FieldInfo fi)
		{
			String typeString = TableGeneratorHelpers.ClrTypeFromSqlType(fi.SqlType)
									.ToString()
									.Replace("System.", "");
			// make nullable version if necessary
			if (fi.AllowsNulls && TableGeneratorHelpers.ClrTypeFromSqlType(fi.SqlType) != typeof(String))
				typeString += "?";

			return typeString;
		}

		// thanks to http://blog.visualt4.com/2009/02/creating-valid-c-identifiers.html
		// needs override mapping
		internal static  string CleanName(string name)
		{
			//Compliant with item 2.4.2 of the C# specification
			System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex(@"[^\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}\p{Nl}\p{Mn}\p{Mc}\p{Cf}\p{Pc}\p{Lm}]");
			string ret = regex.Replace(name, "_"); 
			//The identifier must start with a character
			if (!char.IsLetter(ret, 0))
				ret = string.Concat("_", ret);
			return ret;
		}
	}

	//////////////////////////////////////////////////////////
	// class used for resolving tags in template strings and 
	// recursively using the results as values for tags in other template strings
	// used in the EFCodeGenerator
	internal class TemplateMap : Dictionary<String, String>
	{
		// type to hold tag locations in a string.
		struct TagMap
		{
			internal String Tag;
			internal int Pos;
		}

		// recursively replace tags with values in the input map.
		// ignoreMissingTags = T then a tag with no value is replaced with empty
		// ignoreMissingTags = F throws error
		internal  static String Resolve(String tag, TemplateMap map, bool ignoreMissingTags)
		{
			return Resolve(tag,  map,  ignoreMissingTags, 0);
		}
		
		// The real engine. Level used to prevent circular reference causing a stack overflow.
		private static String Resolve(String tag, TemplateMap map, bool ignoreMissingTags, int level)
		{
			// personal circular tag reference check 
			if (level > 10)
				throw new StackOverflowException("Nesting too deep or circular tag reference situation when resolving tag " + tag);
			// No data for the tag
			if (!map.ContainsKey(tag))
			{
				// no tag = empty
				if (ignoreMissingTags) return ""; 
				// no tag = error
				throw new KeyNotFoundException("Tag "+tag+" missing from map");
			}
			// Safe to get the template like this
			String template = map[tag];
			
			// find all the tags in the string
			TagMap[] tags = ParseTemplate(template);
			
			// process in reverse order so as not to invalidate the positions 
			// obtained from the parse
			for (int idx = tags.GetLength(0) -1; idx >= 0; --idx)
			{
				TagMap t = tags[idx];
				// key is %%tag%% - the %%
				String key = t.Tag.Replace("%%", "");
				// recurse to resolve tag references in the data to replace
				String data = Resolve(key, map, ignoreMissingTags, level + 1);
				// replace the tag with the data
				template = template.Substring(0, t.Pos) + data + template.Substring(t.Pos + t.Tag.Length);
			}
			return template;
		}

		// find all of the tags (%%TAG%%) in a string
		private static TagMap[] ParseTemplate(string template)
		{
			List<TagMap> tags = new List<TagMap>();
			int pos = template.IndexOf( "%%"); 
			if (pos != -1)
			{
				int pos2 = -2;
				while (true)
				{
					pos = template.IndexOf( "%%", pos2 + 2);
					pos2 = template.IndexOf("%%", pos + 2);
					if (pos != -1 && pos2 > pos)
					{
						TagMap tag = new TagMap();
						tag.Pos = pos;
						tag.Tag = template.Substring(pos, pos2 - pos + 2);
						tags.Add(tag);
					}
					else
						break;
				}
			}
			return tags.ToArray();
		}
	}

	//////////////////////////////////////////////////////////
	// Linq POCO generator
	internal class LinqTableGenerator
	{
		public LinqTableGenerator(String tableName, SqlTableSchema schema)
		{
			this.sourceTableName = tableName;
			this.schema = schema;
		}

		//
		// codescope 0 class 1 sample 2 all
		public String GenerateCode(String connectionString, int codeScope)
		{
			// allow for schema 
			String[] tableParts = Helper.ParseTableName(this.sourceTableName);

			String tableName = tableParts[0];
			String linqClassName = TableGeneratorHelpers.MakePascalCaseName(tableName);
			DbView.FieldInfo fi = this.schema[0];

			String classCode = this.GenerateClassCode();
			String sampleCode = String.Format(SAMPLE_TEMPLATE, linqClassName, TableGeneratorHelpers.MakePascalCaseName(fi.Name));
			String moduleCode = String.Format(MODULE_TEMPLATE, classCode, sampleCode, connectionString.Replace("\\", "\\\\"));
			
			return (codeScope == 0) ? classCode : 		
					(codeScope == 1) ? sampleCode + "\r\n" + classCode : 
					moduleCode;
		}

		public String GenerateClassCode()
		{
			String theCode = "";

			List<String> codeLines = new List<String>();  // holds target Code 
			String[] tableParts = Helper.ParseTableName(this.sourceTableName);
			String tableName = tableParts[0];

			// lets define some templates
			// 0 = table name 1 = modified table name 2 = list of column
			// indentation assumes namespace 
			const String FRAMECODE = @"
    // using System.Data.Linq;
    // Linq to SQL Class for table {0}
    [Table( Name = ""{0}"" )]
    public partial class {1}
    {{
{2}
    }}
";
			// 0 = name 1 = type 2 = atts
			String columnTemplate =
@"        [Column( {2} )] public {1} {0} {{get; set;}}
";
			for (int idx = 0; idx < this.schema.Count; ++idx)
			{
				DbView.FieldInfo fi = this.schema[idx];
				String columnParams = this.MakeColumnParams(fi);
				String line = String.Format(columnTemplate, TableGeneratorHelpers.MakePascalCaseName(fi.Name), 
				                                            TableGeneratorHelpers.MakeTypeString(fi), 
															columnParams);
				codeLines.Add(line);
			}

			// assemble
			theCode = String.Format(FRAMECODE, this.sourceTableName, 
											TableGeneratorHelpers.MakePascalCaseName(tableName), 
											String.Join("", codeLines.ToArray()));
			return theCode;
		}

		private string MakeColumnParams(DbView.FieldInfo fi)
		{
			List<String> columnParms = new List<String>();
			columnParms.Add("Name=\"" + fi.Name + "\"");
			if (fi.IsPKey) columnParms.Add("IsPrimaryKey = true");
			if (fi.IsAuto) columnParms.Add("IsDbGenerated = true");
			return String.Join(", ", columnParms.ToArray());
		}

		String sourceTableName;
		SqlTableSchema schema;

		const String MODULE_TEMPLATE = @"
using System;
using System.Linq;
using System.Data.Linq;
using System.Data.Linq.Mapping;

namespace CodeGenTest
{{
    class Program
    {{
        ///////////////////////////////////////////////////////////////
        // Test code 
        static void Main(string[] args)
        {{
            String connectionString = ""{2}"";
            TestClass(connectionString);
        }}
        {1}
    }}
    ///////////////////////////////////////////////////////////////
    // Generated class
    {0}
    ///////////////////////////////////////////////////////////////
}}
";
		const String SAMPLE_TEMPLATE = @"
        private static void TestClass(String connectionString)
        {{
            DataContext db = new DataContext(connectionString);
            // Top 5 of first field only
            foreach({0} rec in db.GetTable<{0}>().Take(5))
            {{
                Console.WriteLine(rec.{1});
            }}
        }}
";
	}

//////////////////////////////////////////////////////////////////////////
//	PetaPoco.Database db = new PetaPoco.Database("Connection String");
//
	internal class PetaPocoTableGenerator
	{
		public PetaPocoTableGenerator(String tableName, SqlTableSchema schema)
		{
			this.sourceTableName = tableName;
			this.schema = schema;
		}

		//
		// codescope 0 class 1 sample 2 all
		public String GenerateCode(String connectionString, int codeScope)
		{
			// allow for schema 
			String[] tableParts = Helper.ParseTableName(this.sourceTableName);

			String tableName = tableParts[0];
			String petaPocoClassName = TableGeneratorHelpers.MakePascalCaseName(tableName);
			DbView.FieldInfo fi = this.schema[0];

			String classCode = this.GenerateClassCode();
			String sampleCode = String.Format(SAMPLE_TEMPLATE, petaPocoClassName, TableGeneratorHelpers.MakePascalCaseName(fi.Name),tableName);
			String moduleCode = String.Format(MODULE_TEMPLATE, classCode, sampleCode, connectionString.Replace("\\", "\\\\"));
			
			return (codeScope == 0) ? classCode : 		
					(codeScope == 1) ? sampleCode + "\r\n" + classCode : 
					moduleCode;
		}

		public String GenerateClassCode()
		{
			String theCode = "";

			List<String> codeLines = new List<String>();  // holds target Code 
			String[] tableParts = Helper.ParseTableName(this.sourceTableName);
			String tableName = tableParts[0];

			// lets define some templates
			// 0 = table name 1 = modified table name 2 = list of column
			// indentation assumes namespace 
			const String FRAME_CODE = @"
    // PetaPoco to SQL Class for table {0}
	[PetaPoco.TableName(""{0}"")]
	[PetaPoco.PrimaryKey(""{3}"")]
    public partial class {1}
    {{
{2}
    }}
";
			// 0 = name 1 = type 2 = atts
			String columnTemplate =
@"        [PetaPoco.Column(""{2}"")] public {1} {0} {{get; set;}}
";
			String primaryKeys = "";
			for (int idx = 0; idx < this.schema.Count; ++idx)
			{
				DbView.FieldInfo fi = this.schema[idx];
				String columnParams = this.MakeColumnParams(fi);
				String line = String.Format(columnTemplate, TableGeneratorHelpers.MakePascalCaseName(fi.Name), 
				                                            TableGeneratorHelpers.MakeTypeString(fi), 
															columnParams);
				codeLines.Add(line);
				if (fi.IsPKey)
				{
					if (primaryKeys.Length > 0) primaryKeys += ",";
					primaryKeys += fi.Name;
				}
			}

			// assemble
			theCode = String.Format(FRAME_CODE, this.sourceTableName, 
											TableGeneratorHelpers.MakePascalCaseName(tableName), 
											String.Join("", codeLines.ToArray()),
											primaryKeys);
			return theCode;
		}

		private string MakeColumnParams(DbView.FieldInfo fi)
		{
			List<String> columnParms = new List<String>();
			columnParms.Add(fi.Name);
//			if (fi.IsPKey) columnParms.Add("IsPrimaryKey = true");
//			if (fi.IsAuto) columnParms.Add("IsDbGenerated = true");
			return String.Join(", ", columnParms.ToArray());
		}

		String sourceTableName;
		SqlTableSchema schema;

		const String MODULE_TEMPLATE = @"
using System;

namespace CodeGenTest
{{
    class Program
    {{
        ///////////////////////////////////////////////////////////////
        // Test code 
        static void Main(string[] args)
        {{
            String connectionString = ""{2}"";
            TestClass(connectionString);
        }}
        {1}
    }}
    ///////////////////////////////////////////////////////////////
    // Generated class
    {0}
    ///////////////////////////////////////////////////////////////
}}
";
		const String SAMPLE_TEMPLATE = @"
        private static void TestClass(String connectionString)
        {{
			String providerName= ""System.Data.SqlClient"";
            PetaPoco.Database db =  new PetaPoco.Database(connectionString, providerName);
            // Top 5 of first field only
			String sql = ""select top 5 * from {2}"";
            foreach({0} rec in db.Query<{0}>(sql))
            {{
                Console.WriteLine(rec.{1});
            }}
        }}
";
	}
}